﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using CodeSandbox.Attributes;
using CodeSandbox.Common;
using CodeSandbox.Contracts;
using CodeSandbox.Models;

namespace CodeSandbox.Controllers
{
    /// <summary>
    /// Defines a sandbox environment which records all vital statistics of the execution.
    /// </summary>
    [DataContract]
    //[DebuggerStepThrough]
    public class ExecutionSandbox : ISandbox
    {
        private readonly ICallerAgentProvider _caller;
        private readonly string _dbServerName;
        private static object lockObj = new object();


        /// <summary>
        /// 
        /// </summary>
        /// <param name="contextId"></param>
        /// <param name="provider"></param>
        /// <param name="callerFile"></param>
        /// <param name="callerName"></param>
        /// <param name="callerLine"></param>
        /// <param name="logDbServer"></param>
        public ExecutionSandbox(Guid contextId, ICallerAgentProvider provider, string callerFile, string callerName, int callerLine, string logDbServer):this()
        {
            if (Sandbox.SupressOutput)
                _caller = new DefaultCallerAgent();
            else
                _caller = provider ?? new DefaultCallerAgent();
            Tabs = string.Join("", Enumerable.Repeat("\t", Sandbox.ActiveSandboxes.Count));
            Sandbox.ActiveSandboxes.Add(this);
            _caller.Output.SendStatement("{0}Executing:{1} at {2}", Tabs, callerName, DateTime.Now);
            Initialize();
            CallStartUtcTime = DateTime.UtcNow;
            ContextId = contextId;
            CallerFile = callerFile;
            CallerName = callerName;
            CallerLine = callerLine;
            _dbServerName = logDbServer;
            IsOpen = true;

        }
        public ExecutionSandbox()
        { }
        #region Properties

        /// <summary>
        /// Represents if the sandbox is still active and code is executing.
        /// </summary>
        [DataMember]
        [XmlAttribute]
        public bool IsOpen { get; set; }

        /// <summary>
        /// Company name which owns the code block.
        /// </summary>
        [DataMember]
        [XmlAttribute]
        public string Company { get; set; }

        /// <summary>
        /// Division name which owns the code block.
        /// </summary>
        [DataMember]
        [XmlAttribute]
        public string Division { get; set; }

        /// <summary>
        /// Machine name which ran the code block
        /// </summary>
        [DataMember]
        [XmlAttribute]
        public string MachineName { get; set; }

        private string _application;

        /// <summary>
        /// The application which is executing the code block
        /// </summary>
        [DataMember]
        [XmlAttribute]
        public string Application
        {
            get { return _application; }
            set
            {
                _application = value;
            }
        }

        [DataMember]
        [XmlAttribute]
        public string ComponentTitle { get; set; }

        [DataMember]
        [XmlAttribute]
        public string ComponentFile { get; set; }

        [DataMember]
        [XmlAttribute]
        public Guid ContextId { get; set; }

        [DataMember]
        [XmlAttribute]
        public string CallerFile { get; set; }

        [DataMember]
        [XmlAttribute]
        public string CallerName { get; set; }

        /// <summary>
        /// 
        /// </summary>
        private string _callingUser;
        [DataMember]
        [XmlAttribute]
        public string CallingUser
        {
            get { return _callingUser ?? string.Format("{0}\\{1}", Environment.UserDomainName, Environment.UserName); }
            set { _callingUser = value; }
        }

        [DataMember]
        [XmlAttribute]
        public string CallerIp { get; set; }

        [DataMember]
        [XmlAttribute]
        public int CallerLine { get; set; }

        private object _returnValue=null;
        [DataMember]
        public object ReturnValue {
            get { return _returnValue; } 
            set{} 
        }

        [DataMember]
        [XmlAttribute]
        public SandboxState FinalStatus { get; set; }


        [DataMember]
        public List<Exception> FailureExceptions
        {
            get { return _failureExceptions; }
            set { }
        }



        [DataMember]
        public List<Exception> IgnoredExceptions
        {
            get { return _ignoredExceptions; }
            set { }
        }

        [DataMember]
        [XmlAttribute]
        public DateTime CallStartUtcTime { get; set; }

        [DataMember]
        [XmlAttribute]
        public DateTime CallEndUtcTime { get; set; }

        [DataMember]
        public List<CallArgument> CallArguments
        {
            get { return _callArguments; }
            set { }
        }


        [XmlIgnore]
        public IValidator Ensure { get; private set; }

        [XmlIgnore]
        public IHelper Help { get; private set; }

        [XmlIgnore]
        public IExecutor Kick { get; private set; }

        [XmlIgnore]
        public IDependencyLibrary Library { get; private set; }

        [XmlIgnore]
        public IMapper Mapper { get; private set; }

        [DataMember]
        public List<LogicalStep> RecordedSteps
        {
            get { return _recordedSteps; }
            set { }
        }



        public void LogArguments(params object[] parameters)
        {
            int i = 1;
            _callArguments.AddRange(parameters.Select(p => new CallArgument { Name = string.Format("Argument {0}", i++), Value = p }));
            _caller.Output.SendSuccess("{0}Parameters are logged successfully.", Tabs);
        }




        #endregion

        #region Public Methods

        public XElement AsXml()
        {
            return GetXml(this);
        }

        public void TagAsStatement(string statement, [CallerFilePath] string callerFile = @"N/A", [CallerMemberName] string callerName = @"N/A", [CallerLineNumber] int callerLine = 0)
        {
            if (Sandbox.LogMode == LogMode.OnlyWarnings || Sandbox.LogMode == LogMode.OnlyFailures || Sandbox.LogMode == LogMode.FailuresAndWarnings || Sandbox.LogMode == LogMode.BasicCallInfo || Sandbox.LogMode == LogMode.None) return;
            _recordedSteps.Add(new LogicalStep { StepNumber = _recordedSteps.Count + 1, ExecutedAt = DateTime.UtcNow, CallerFile = callerFile, CallerLine = callerLine, CallerName = callerName, Message = statement, StepType = LogType.Information });
            _caller.Output.SendStatement("{0}Tagged statement: [Line {3}:]  {1} at {2}", Tabs, statement, DateTime.Now, callerLine);
        }

        public void TagAsSuccededStep(string successMessage = null, [CallerFilePath] string callerFile = @"N/A", [CallerMemberName] string callerName = @"N/A", [CallerLineNumber] int callerLine = 0)
        {
            if (Sandbox.LogMode == LogMode.OnlyWarnings || Sandbox.LogMode == LogMode.OnlyFailures || Sandbox.LogMode == LogMode.FailuresAndWarnings || Sandbox.LogMode == LogMode.BasicCallInfo || Sandbox.LogMode == LogMode.None) return;
            _recordedSteps.Add(new LogicalStep { StepNumber = _recordedSteps.Count + 1, ExecutedAt = DateTime.UtcNow, CallerFile = callerFile, CallerLine = callerLine, CallerName = callerName, StepType = LogType.Succeded });
            _caller.Output.SendSuccess("{0}Tagged success: [Line {3}:] {1} at {2}", Tabs, successMessage, DateTime.Now, callerLine);
        }

        public void TagAsFailedStep(string failureMessage = null, [CallerFilePath] string callerFile = @"N/A", [CallerMemberName] string callerName = @"N/A", [CallerLineNumber] int callerLine = 0)
        {
            if (Sandbox.LogMode == LogMode.OnlyInformation || Sandbox.LogMode == LogMode.OnlyWarnings || Sandbox.LogMode == LogMode.BasicCallInfo || Sandbox.LogMode == LogMode.None) return;
            _recordedSteps.Add(new LogicalStep { StepNumber = _recordedSteps.Count + 1, ExecutedAt = DateTime.UtcNow, CallerFile = callerFile, CallerLine = callerLine, CallerName = callerName, StepType = LogType.Failure });
            _caller.Output.SendFailure("{0}Tagged failure: [Line {3}:]  {1} at {2}", Tabs, failureMessage, DateTime.Now, callerLine);
        }

        public void TagAsWarningStep(string warningMessage = null, [CallerFilePath] string callerFile = @"N/A", [CallerMemberName] string callerName = @"N/A", [CallerLineNumber] int callerLine = 0)
        {
            if (Sandbox.LogMode == LogMode.OnlyInformation || Sandbox.LogMode == LogMode.OnlyFailures || Sandbox.LogMode == LogMode.BasicCallInfo || Sandbox.LogMode == LogMode.None) return;
            _recordedSteps.Add(new LogicalStep { StepNumber = _recordedSteps.Count + 1, ExecutedAt = DateTime.UtcNow, CallerFile = callerFile, CallerLine = callerLine, CallerName = callerName, StepType = LogType.Warning });
            _caller.Output.SendWarning("{0}Tagged warning: [Line {3}:]  {1} at {2}", Tabs, warningMessage, DateTime.Now, callerLine);
        }

        public void LogIgnoredException(Exception exception)
        {
            if (Sandbox.LogMode == LogMode.OnlyInformation || Sandbox.LogMode == LogMode.OnlyFailures || Sandbox.LogMode == LogMode.BasicCallInfo || Sandbox.LogMode == LogMode.None) return;
            _ignoredExceptions.Add(exception);
            _caller.Output.SendWarning("{0}An exception with message {1} has been ignored.", Tabs, exception.Message);
        }

        public void LogFailureException(Exception exception)
        {
            if (Sandbox.LogMode == LogMode.OnlyInformation || Sandbox.LogMode == LogMode.OnlyWarnings || Sandbox.LogMode == LogMode.BasicCallInfo || Sandbox.LogMode == LogMode.None) return;
            _failureExceptions.Add(exception);
            _caller.Output.SendWarning("{0}An exception with message {1} has been registered.", Tabs, exception.Message);
        }

        public void LogReturnValue(object returnValue)
        {
            _returnValue = returnValue;
            _caller.Output.SendSuccess("{0}Return value has been noted. See log.", Tabs);
        }

        public void SafeRun<TReturn>(Func<TReturn> logic)
        {
            throw new NotImplementedException();
        }

        public void RunAndIgnore<TReturn, TKnownException>(Func<TReturn> logic)
        {
            throw new NotImplementedException();
        }

        public void RunAndExpect<TReturn, TExpectedException>(Func<TReturn> logic)
        {
            throw new NotImplementedException();
        }

        public TContent UnBox<TContent>(object box)
        {
            if (box == null) throw new ArgumentNullException("box");
            return (TContent)box;
        }

        public void NotNull<TContent>(Expression<Func<TContent>> dataToValidate) where TContent : class
        {
            if (dataToValidate == null || (dataToValidate.Compile().Invoke() != null && dataToValidate.Compile().Invoke().GetType().IsValueType)) throw new ArgumentNullException("dataToValidate");
            var body = dataToValidate.Body as MemberExpression ?? ((UnaryExpression)dataToValidate.Body).Operand as MemberExpression;
            if (body == null) throw new InvalidDataException("Invalid null check");
            var memberName = body.Member.Name;
            if (dataToValidate.Compile().Invoke() == null)
            {
                TagAsFailedStep(memberName);
                throw new ArgumentNullException(memberName);
            }
            _caller.Output.SendSuccess("{0}required parameter {1} has value", Tabs, memberName);
        }

        public void NotDefault<TContent>(Expression<Func<TContent>> dataToValidate)
        {
            if (dataToValidate == null || !dataToValidate.Compile().Invoke().GetType().IsValueType) return;
            var body = dataToValidate.Body as MemberExpression ?? ((UnaryExpression)dataToValidate.Body).Operand as MemberExpression;
            if (body == null) throw new InvalidDataException("Invalid null check");
            var memberName = body.Member.Name;
            if (dataToValidate.Compile().Invoke().Equals(default(TContent)))
            {
                TagAsFailedStep(memberName);
                throw new ArgumentException(memberName);
            }
            _caller.Output.SendSuccess("{0}required parameter {1} has value", Tabs, memberName);
        }

        public void That(Expression<Func<bool>> dataToValidate)
        {
            if (dataToValidate == null) throw new ArgumentNullException("dataToValidate");
            var memberName = string.Join(" ", dataToValidate.Body.ToString()
                            .Split(new[] { "=>" }, StringSplitOptions.RemoveEmptyEntries)[0]
                            .Split(new[] { " " }, StringSplitOptions.None)
                            .Select(s => (!string.IsNullOrEmpty(s) && s.Contains(").")) ? s.Substring(s.LastIndexOf(").", StringComparison.InvariantCulture), s.Length - s.LastIndexOf(").", StringComparison.InvariantCulture)) : s))
                            .Replace(").", "(");
            if (!dataToValidate.Compile().Invoke())
            {
                var message = string.Format("Data doesn't satisfy the required condition {0}", memberName);
                TagAsFailedStep(message);
                throw new InvalidDataException(message);
            }
            _caller.Output.SendSuccess("{0}Condition:{1} is satisfied", Tabs, memberName);
        }

        public new void Dispose()
        {
            IsOpen = false;
            CallEndUtcTime = DateTime.UtcNow;
            //LogMe();
            Task.Factory.StartNew(LogMe);
        }

        #endregion

        #region Private Members

        private List<CallArgument> _callArguments;
        private List<Exception> _failureExceptions;
        private List<Exception> _ignoredExceptions;
        private List<LogicalStep> _recordedSteps;
        private readonly string Tabs;

        private void Initialize()
        {
            var thisAssembly = Assembly.GetEntryAssembly() ?? GetCallingAssembly();
            if (thisAssembly != null)
            {
                var companyAttributes = thisAssembly.GetCustomAttributes(typeof(AssemblyCompanyInfoAttribute), true);
                if (companyAttributes.Any())
                    Company = string.IsNullOrWhiteSpace(Sandbox.Company)
                        ? UnBox<AssemblyCompanyInfoAttribute>(companyAttributes[0]).CompanyName
                        : Sandbox.Company;
                if (companyAttributes.Any())
                    Division = string.IsNullOrWhiteSpace(Sandbox.Division)
                        ? UnBox<AssemblyCompanyInfoAttribute>(companyAttributes[0]).DivisionName
                        : Sandbox.Division;
                MachineName = Environment.MachineName;
                var productAttributes = thisAssembly.GetCustomAttributes(typeof(AssemblyProductAttribute), true);
                if (productAttributes.Any())
                    Application = string.IsNullOrWhiteSpace(Sandbox.Application)
                        ? UnBox<AssemblyProductAttribute>(productAttributes[0]).Product
                        : Sandbox.Application;
                ComponentTitle = thisAssembly.FullName;
                ComponentFile = thisAssembly.Location;
            }
            _failureExceptions = new List<Exception>();
            _ignoredExceptions = new List<Exception>();
            _callArguments = new List<CallArgument>();
            _recordedSteps = new List<LogicalStep>();
            Ensure = this;
            Help = this;
            Kick = this;
            Library = new DependencyLibrary();
            Mapper = new MappingEngine();
        }

        private Assembly GetCallingAssembly()
        {
            var file = new FileInfo(new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase).LocalPath);
            if (!string.IsNullOrWhiteSpace(file.DirectoryName))
            {
                try
                {
                    return Directory.GetFiles(file.DirectoryName, "*.dll")
                        .Select(Assembly.LoadFile)
                        .First(a => a.GetCustomAttributes(typeof(AssemblyCompanyInfoAttribute), true).Any());
                }
                catch (Exception) { }

            }
            return null;
        }

        private XElement GetXml(object data)
        {
            var list = data as IEnumerable;
            if (list == null || data is string) return SerializeObject(data);
            XElement rootElement = XElement.Parse("<List/>");
            foreach (var objectToSerialize in list)
            {
                var element = SerializeObject(objectToSerialize);

                rootElement.Add(element);
            }
            return rootElement;
        }
        private XElement SerializeObject(object data)
        {
            if (data == null) return null;

            using (var stringWriter = new StringWriter())
            {
                using (var writer = new XmlTextWriter(stringWriter))
                {
                    try
                    {
                        var serializer = new DataContractJsonSerializer(data.GetType(), new List<Type>(new[] { typeof(DataSet), typeof(DataTable),typeof(DataRow),typeof(DataColumn)}));
                        serializer.WriteObject(writer,data);
                    }
                    catch (Exception)
                    {
                        try
                        {
                            var serializer = new DataContractSerializer(data.GetType());

                            serializer.WriteObject(writer, data);
                        }
                        catch (Exception)
                        {
                            var element = new XElement("PartialObjectInfo") { Value = data.ToString() };
                            return element;
                        }
                    }

                    writer.Flush();
                    return RemoveDefaultNamespaces(XElement.Load(new StringReader(stringWriter.ToString())));
                }
            }
        }

        private XElement RemoveDefaultNamespaces(XElement element)
        {
            foreach (var attribute in element.Attributes()) if (attribute.Name.LocalName == "xsd") attribute.Remove();
            foreach (var attribute in element.Attributes()) if (attribute.Name.LocalName == "xsi") attribute.Remove();
            foreach (var node in element.Elements())
            {
                RemoveDefaultNamespaces(node);
            }
            return element;
        }

        private void WriteToEventLog(string message)
        {
             if(!EventLog.SourceExists(Sandbox.Application)) EventLog.CreateEventSource(Sandbox.Application,"Application");
             EventLog.WriteEntry(Sandbox.Application, message, FinalStatus == SandboxState.Succeded ? EventLogEntryType.Information :
                        FinalStatus == SandboxState.Warning || FinalStatus == SandboxState.SuceededWithWarning ? EventLogEntryType.Warning : EventLogEntryType.Error
                       ,Thread.CurrentThread.ManagedThreadId,(short)FinalStatus);
        }

        private void WriteToLocalFile(string fileName, string message)
        {
            var logFile = Path.IsPathRooted(fileName)? fileName: string.Format(@"{0}\{1}_{2}.xml", Directory.GetCurrentDirectory(), CallerName, Guid.NewGuid());
            TagAsWarningStep(string.Format("Logged to the local file at {0}.", logFile));
            if (File.Exists(logFile))
            {
                Sandbox.LogWriteLock.AcquireWriterLock(60000);
                try
                {
                    File.AppendAllText(logFile, string.Format("{0}<!--{1}-->{2}{3}", Environment.NewLine, new string('=', 100), Environment.NewLine, message));
                    var tempLog = string.Format("{0}_temp",logFile);
                    if (Sandbox.RolloverSizeInMb > 0 && new FileInfo(logFile).Length/1024/1024 >= Sandbox.RolloverSizeInMb)
                    {
                        using (StreamReader reader = new StreamReader(logFile))
                        {
                            using (StreamWriter writer = new StreamWriter(tempLog))
                            {
                                string breakLine;
                                int breakPos = 0;
                                while ((breakLine = reader.ReadLine()) != null)
                                {
                                    if (breakLine.Contains(string.Format("<!--{0}-->", new string('=', 100))))
                                        breakPos++;
                                    if (breakPos >= 5) writer.WriteLine(breakLine);
                                }
                            }
                        }
                        if (new FileInfo(tempLog).Length > 0)
                        {
                            File.Delete(logFile);
                            File.Move(tempLog, logFile);
                        }
                        else
                            File.Delete(tempLog);
                    }

                }
                finally 
                {
                    Sandbox.LogWriteLock.ReleaseWriterLock();
                }
            }
            else
            File.WriteAllText(logFile, message);
        }

        private void LogToDestination(LogDestination destination)
        {
            switch (destination)
            {
                case LogDestination.EventLog:
                    {
                        WriteToEventLog(this.SerializeTo<string>());
                        break;
                    }
                case LogDestination.LocalFile:
                    {
                        WriteToLocalFile(Sandbox.LocalLogFile, this.SerializeTo<string>(typeof(ILogData)));
                        break;
                    }
                case LogDestination.UserFunction:
                    {
                        if (Sandbox.CustomLogWriter != null)
                        {
                            lock (lockObj)
                            {
                                Sandbox.CustomLogWriter(this);
                            }
                            TagAsSuccededStep("Successfully logged to the database");
                        }
                        break;
                    }
                case LogDestination.FailoverSequence:
                    {
                        foreach (var logDestination in Sandbox.FailoverSequence)
                        {
                            if(logDestination == LogDestination.FailoverSequence) continue;
                            try
                            {
                                LogToDestination(logDestination);
                                break;
                            }
                            catch (Exception)
                            {
                                // ignored
                            }
                        }
                        break;
                    }
            }
        }

        private void LogMe()
        {
            if (Sandbox.LogMode == LogMode.None) return;

            try
            {
                LogToDestination(Sandbox.LogDestination);

            }
            catch (Exception exp)
            {
                try
                {
                    WriteToLocalFile(null, this.SerializeTo<string>());
                }
                catch (Exception exception)
                {
                    TagAsWarningStep(
                        string.Format("Failed all attempts to write log. Log data will be lost. Error: {0}",
                                      exception.Message));
                }
            }
            _caller.Output.SendStatement("{0}Executed:{1} at {2}", Tabs, CallerName, DateTime.Now);
            Sandbox.ActiveSandboxes.Remove(this);
        }

        #endregion
    }
}
